ANDROID | 自定义VIEW入门

一、自定义View

Android系统内置的View无法满足业务需求,需要自定义。

Step 1 : 继承View

1
2
3
4
5
6
7
8
9
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}

至少写2个构造函数

Step 2 : 自定义属性

有些属性希望由用户指定,只有当用户不指定的时候才用我们硬编码的值,比如默认的宽高,在res/values/styles.xml文件里声明:

1
2
3
4
5
6
7
8
<resources>
<!-- suggestion:the name is the class name -->
<declare-styleable name="CustomView">
<attr name="default_size" format="dimension" />
</declare-styleable>
</resources>

布局文件activity_main.xml中使用自定义的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xian="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- xian为命名空间,名称随便定义,其值固定为"http://schemas.android.com/apk/res-auto" -->
<com.king.demo.customview.CustomView
android:layout_width="match_parent"
android:layout_height="100dp"
xian:default_size="100dp" />
</LinearLayout>

构造器中读取配置信息并初始化

1
2
3
4
5
6
7
8
private int defaultSize;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
defaultSize = a.getDimensionPixelSize(R.styleable.CustomView_default_size, 100);
a.recycle(); // 回收TypedArray对象
}

Step 3 : 重写onMeasure()

测量宽高尺寸并设置需要的值。如果不需要制定自定义控件的宽高,无需重写此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 重构控件宽高,此处设宽高相等
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height)
height = width;
else
width = height;
setMeasuredDimension(width, height);
}
/**
* 辅助方法,计算宽高
* @param defaultSize 设置宽高默认的值
* @param measureSpec
* @return
*/
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: // 如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
case MeasureSpec.AT_MOST: // 如果测量模式是最大取值为size
case MeasureSpec.EXACTLY: // 如果是固定的大小,那就不要去改变它
mySize = size;
break;
}
return mySize;
}

  • onMeasure()至少会被调用两次,第一次读取xml文件中的参数layout_width和layout_width,它们可以不用指定具体的尺寸,如值为wrap_content或match_parent。为了更好的适配各种尺寸的屏幕,需要根据父类布局或子类控件宽高动态计算实际宽高,而不是在xml中指定具体的数值。
  • widthMeasureSpec和heightMeasureSpec都是int,前面2个bit用于区分不同的测量模式,后面30个bit存放的是尺寸的数据。测量模式分三种:UNSPECIFIED(父容器没有对当前View有任何限制,当前View可以任意取尺寸)、EXACTLY(当前的尺寸就是当前View应该取的尺寸,对应match_parent和固定值)、AT_MOST(当前尺寸是当前View能取的最大尺寸,对应wrap_content)

Step 4 : 重写onDraw()

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;
int centerX = getLeft() + r;
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(centerX, centerY, r, paint);
}

效果图

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.king.demo.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
/**
* 继承View,至少写2个构造函数
*/
public class CustomView extends View {
private int defaultSize;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
defaultSize = a.getDimensionPixelSize(R.styleable.CustomView_default_size, 100);
a.recycle(); // 回收TypedArray对象
}
/**
* 辅助方法,计算宽高
* @param defaultSize 设置宽高默认的值
* @param measureSpec
* @return
*/
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: // 如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY: // 如果是固定的大小,那就不要去改变它
mySize = size;
break;
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 重构控件宽高
int width = getMySize(defaultSize, widthMeasureSpec);
int height = getMySize(defaultSize, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int r = getMeasuredWidth() / 2;
int centerX = getLeft() + r;
int centerY = getTop() + r;
Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(centerX, centerY, r, paint);
}
}

二、自定义ViewGroup

模拟一个简易垂直布局,详情可参考Android内置的LinearLayout源码。

Step 1 : 继承ViewGroup

必须实现onLayout(boolean changed, int left, int top, int right, int bottom)方法,用于“摆放”各个子View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Created by xianxiaotao on 17/3/11.
*/
public class SimpleLinearLayout extends ViewGroup {
public SimpleLinearLayout(Context context) {
super(context);
}
public SimpleLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO
}
}

Step 2 : 自定义属性(同上)

Step 3 : 重写onMeasure()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec); // 触发每个子View的onMeasure方法
int count = getChildCount();
int maxWidth = 0;
int height = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child == null || child.getVisibility() == View.GONE)
continue;
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
maxWidth = childWidth > maxWidth ? childWidth : maxWidth;
height += childHeight; // 不考虑Margin
}
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.AT_MOST)
maxWidth = MeasureSpec.getSize(widthMeasureSpec);
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.AT_MOST)
height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(maxWidth, height);
}

Step 4 : 重写onLayout()

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int curHeight = t; // 记录当前的高度位置
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int height = child.getMeasuredHeight();
int width = child.getMeasuredWidth();
child.layout(l, curHeight, l + width, curHeight + height);
curHeight += height;
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xian="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.king.demo.customview.SimpleLinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00FFFF">
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="75dp"
android:layout_height="wrap_content"
android:text="btn" />
</com.king.demo.customview.SimpleLinearLayout>
</LinearLayout>

效果图